#本文件定义 server sock主类，完成server run ,数据收发和交换

import socket,select
import base
import message
import serversql

class serverrun(base.base,message.msg):
    def __init__(self,s_address):
        super(serverrun,self).__init__()
        try:
            self.__so= socket.socket(socket.AF_INET,socket.SOCK_STREAM)
            self.__so.bind(s_address)
            self.__address = s_address
            self.__so.listen()
            self._log('server%s is listening...'%str(s_address),self.LOG_INFO)
        except socket.error as e:
            self._log(str(e),self.LOG_WARN)
            return
        #定义run中需要使用的变量，也可以放在run方法中定义
        # so选择列表改为全局，异常子函数调用
        self.__rsolist = [] # 定义读套接字列表，用于监听select
        self.__wsolist = [] # 定义写套接字列表
        self.__esolist = [] # 定义错套接字列表

        # 定义连接字典，保存client连接内容，为四层数据，sessionid意义不明显，这里取消，采用clientso作为key
        # {so:{'addr':c_addr,'leftbys':left_bytes,'id':id}} 为每个连接保存一个left_bytes,用于接收拼接
        #id为应用层id，在用户上线时填入
        self.__conndict = {}

        #定义id连接字典，维护上线id和其so对应关系，用于data交换使用
        #{id: so}
        self.__idconndict = {}

        # 定义成员属性字典，动态记录上线用户的基本属性，目前为增加name
        #{id:{'name':name}}
        self.__memberdict = {} #member/person语义待斟酌。。
        self.__mysql = serversql.talksql() #数据库对象

    def server_send(self,id,send_bytes):
        pass

    #定义一些接口用于显示动态数据
    def showconn(self):#显示连接表
        print('here are connect diction')
        print(self.__conndict)

    def showmember(self): #显示成员表
        print('here are member diction')
        print(self.__memberdict)

    def showidconn(self):#显示id conn交换表member
        print('here are id connect diction')
        print(self.__idconndict)

    #一下定义私有方法提供如成员认证等应用处理
    def __proc_uplistreq(self,so,id):

        #由于这里是用户tcp重连时重新发这个“第一个”消息更新用户表，这里通过它刷新idconn，更新其so信息，便于后续通讯
        if self.__idconndict.get(id):
            self.__idconndict.update({id:so})
        uplist = []
        dictkeys = self.__idconndict.keys()
        for up_id in dictkeys:
            # 通知该当前上线用户，新用户上线
            up_name = self.__memberdict.get(up_id).get('name')
            uperlist = [up_id, up_name]
            uplist.append(uperlist)

        send_bytes = self.msgpack_c_current_uplist(uplist)
        self._log('send current uplist: %s' % (str(uplist)))
        self.__sosendall(so, send_bytes)

    def  __auth(self,so,id,password):
        self.__mysql.open()
        try:
            pw,name = self.__mysql.getmember(id)
        except :
            send_bytes = self.msgpack_c_auth_rej(0)
            self._log("anth rej:id =%d,reason is 'id is not in mysql'."%id)
            self.__sosendall(send_bytes)
            self.__mysql.close()
            return
        finally:
            self.__mysql.close()

        #以下处理认证
        if password == pw:
            #判断是否重复连接，目前不支持重复连接
            '''暂时打开
            if self.__memberdict.get(id) is not None:
                send_bytes = self.msgpack_c_auth_rej(3)
                self._log("anth rej:id =%d,reason is 'can not double login'." % id)
                self.__sosendall(so,send_bytes)
                return
            '''

            #先判断该用户是不是重复登录，如果是，要查一下conndict中是否存在该用户，如果存在则将其conn down掉
            dictkeys = self.__conndict.keys()
            for key in dictkeys:
                if self.__conndict.get(key).get("id") == id:
                    if key is not so:
                        self.__conndict.pop(key)
                        self._log("conn %s has been popped from conndict." %str(key))
                        self.__rsolist.remove(key)
                        self.__wsolist.remove(key)
                        self.__esolist.remove(key)
                        key.close();#这里把系统的socket关闭
                        #这里可能要删除rlist,wlist和xlist避免动态处理错误
                        break
            send_bytes = self.msgpack_c_auth_ack(name)
            self._log("anth ack:id =%d" % id)
            self.__sosendall(so,send_bytes)
            #用户上线更新成员表，id连接表和连接表
            self.__memberdict.update({id:{'name':name}})
            self._log("member %d has been added to memberdict." % id)
            self.__idconndict.update({id:so})
            self._log("(id:%d) and which 'so' has been added to idconndict."%id)
            self.__conndict.get(so).update({'id':id})
            self._log("(id:%d) has been set into %s conndict"%(id,str(so.getpeername())))

            # 通知用户uplist,在idconndict中遍历
            uplist = []
            dictkeys = self.__idconndict.keys()
            for up_id in dictkeys:
                up_so = self.__idconndict.get(up_id)
                # 通知该当前上线用户，新用户上线
                send_bytes = self.msgpack_c_notify_new_up([id, name])
                self.__sosendall(up_so,send_bytes)
                self._log('nodity new up [id =%d,name =%s]'%(id,name))

                up_name = self.__memberdict.get(up_id).get('name')

                uperlist = [up_id, up_name]
                uplist.append(uperlist)

            send_bytes = self.msgpack_c_current_uplist(uplist)
            self._log('send current uplist: %s'%(str(uplist)))
            self.__sosendall(so, send_bytes)
        else:
            send_bytes = self.msgpack_c_auth_rej(0)
            self._log("anth rej:id =%d, reason is 'password is not right'." % id)
            self.__sosendall(so,send_bytes)


    #定义以下私有方法提供socket send，recv封装，以集中捕获和处理异常，供cientso处理使用
    #返回值保持系统原生
    def __soexceptdo(self,so):
        #此处在sorecv和sosend时可能会发生，但对于同一个so，可能存在重复发生，所以以下处理要避免重复调用的异常
        #增加socket xlist
        #在每个处理前增加数据合法性，比如是否存在的判断

        # 出现异常，这里表明这个客户的四层连接出现异常，处理如下：
        # 删除select表，删除连接表，删除交换表，删除成员表，-----这样做的语义是：底层（四层）连接断了，则上层的连接归零，
        # 接上，目前为：认证过程归零，用户需要重新提交用户名，密码完成认证登录。

        if so in self.__rsolist:  # 这里加判断，避免下述问题
            self.__rsolist.remove(so)  # ？？？此处出现so不在list中的异常 有可能是recv时已经异常并删除了，这里又发送再删除时故障
            self._log("so(%s) has been removed from rsolist " % str(so.getpeername()))
        if so in self.__wsolist:
            self.__wsolist.remove(so)
            self._log("so(%s) has been removed from wsolist" % str(so.getpeername()))
        if so in self.__esolist:
            self.__esolist.remove(so)
            self._log("so(%s) has been removed from esolist" % str(so.getpeername()))
            so.close()#避免重复close放在这里

        # 语义修改如下，连接断，不代表ft用户会话断，因此不删除成员表和idconn，只刷新idconn中的so为none
        #用户会话断对应于用户的取消登录操作,TCP断对应登录用户的idconn表中为空，在查找交换时失败，不能交换
        #TCP断对应无登录用户无影响，是操作TCP模块处理
        if so in self.__conndict.keys():
            id = self.__conndict.get(so).get('id')
            self.__conndict.pop(so)
            self._log("so %s conndict has been poped." % str(so))
            if id is not None:  # 这里表明该用户已经认证上线，下述表存在
                self.__idconndict.update({id:None})
                self._log("%d 's idconndict so has been set to none." % id)
        return -1


    def __sosendall(self,so,send_bytes):
        if so is None:
            self._log('__sosendall so is None', self.LOG_ERROR)
            return
        try:
            r= so.sendall(send_bytes)
            self._log('send data:%s' % send_bytes, self.LOG_DEBUG)
            return r
        except socket.error as e:
            #这里发生的语义？：在服务器处理so发送的时候，客户端异常断了（包括主动down或者拔除网络线）
            #客户端异常断了，对于服务器存在两种情况，一种是检测到，一种是没检测到，
            # 1. 客户的主动down，会发送fin报文给服务器，服务器收到了，并在应用recv的时候通知，在应用还未处理该通知时，
            # 操作系统TCP状态机可能会完成了down，这样客户调用socksend时就感知到这个异常了，所以这里要处理.
            #2. 客户端异常，拔除网线，这时服务器并不能检测到，服务器的tcp状态机一直正常，而应用调用socketsend，发送会
            #成功，直到tcp发送缓存占满，而后socksend返回异常，这种情况，这里也会感知到。
            #因此这里的处理应该基于以上两种情况判断异常，是为清晰。对于前者，则作为四层连接down处理，删除动态表项，对于
            #后者占满，并不代表错误，所以不能立即删除动态表，需要补充其他的机制最终确定用户down后处理。
            #对于占满的出现使用操作系统socket时候有阻塞和非阻塞处理，方式，阻塞式是得不到的，只会让应用进程暂停，非阻塞式会
            #得到

            #目前为阻塞socket接口调用，故，这里为1的情况，所以删除动态表。对于阻塞对其他连接用户的影响的处理待下一步分析
            self._log("socket send exception. %s"%str(e), self.LOG_ERROR)

            return self.__soexceptdo(so)

    def __sorecv(self,so,bufflen):
        if so is None:
            self._log('__sorecv so is None', self.LOG_ERROR)
            return -1
        try:
            r_bytes =  so.recv(bufflen)
            if len(r_bytes):
                self._log('recv data:%s' %r_bytes,self.LOG_DEBUG)
            return r_bytes
        except socket.error as e:
            self._log("socket recv exception. %s" % str(e), self.LOG_ERROR)
            # 出现异常，这里表明这个客户的四层连接出现异常，处理如下：
            # 删除select表，删除连接表，删除交换表，删除成员表，-----这样做的语义是：底层（四层）连接断了，则上层的连接归零，
            # 接上，目前为：认证过程归零，用户需要重新提交用户名，密码完成认证登录。
            return self.__soexceptdo(so)

    def server_run(self):
        self.__rsolist.append(self.__so)
        #以下执行server run体，循环select并遍历处理收发和error
        while True:
            rlist, wlist, xlist = select.select(self.__rsolist, self.__wsolist, self.__esolist)
            self.__proc_rlist(rlist,wlist)#注意rsolist,wsolist为监听list，rlist,wlist为结果list
            self.__proc_xlist(xlist)

    #该方法处理select后的rlist结果集
    def __proc_rlist(self,rlist,wlist):
        for ro in rlist:#注意：遍历处理中可能引发调用其他so（该so可能是在rlist处理的后续）异常导致so被关闭了，因此在处理中要增加相应保护
            if ro == self.__so:#如果为当前server的侦听sock
                so_c, c_addr = self.__so.accept()
                # 将客户sock分别加入读写list进行监视
                self.__rsolist.append(so_c)
                self.__wsolist.append(so_c)
                self.__esolist.append(so_c)

                self._log('here accept a client: %s.'%str(c_addr))
                # 生成该客户的连接记录并加入连接字典：{so:{'addr':c_addr,'leftbys':left_bytes}}
                self.__conndict.update({so_c: {'addr':c_addr,'leftbys':bytes()}})
                self._log('client : %s so, has been added to the connect diction.'%str(c_addr),self.LOG_INFO)

            else:
                # 这里为客户sock，表示客户sock可读
                # 一次最大接收1024字节，若未收完，等待下次处理
                recv_bytes = self.__sorecv(ro,1024)
                if recv_bytes == -1: #so 接收异常
                    break
                # 解析recv bytes，因为tcpsocket为流，故可能recvbytes中可能有多个消息，需要循环截取操作
                # 下面代码还要增加各个字段获取时的长度判断，原因是tcp是流，在每次截取消息时可能会不够长度,
                # 另外如果长度不够则无法进行处理，但是不是代表是错误的，后续的recv可能会补全后面的bytes
                # 因此这里要好好设计，考虑到这里采用消息传送，包含控制消息和用户会话消息在一起，因此可以考虑
                # 按一个消息进行处理，即满足一个消息完整处理则处理，否则记下这个消息（这里需要针对每一个连接设计一个剩余包）
                # ，然后跟下一次的recv拼接，直到它接收完整，才完整处理一次，另外，每次收发recv的长度也要考虑，假如一次未收全，
                # 那还有剩下的会存留在sock缓冲中，等待下一次处理

                # 获取该连接的left_bytes
                #left_bytes = self.__conndict[ro]['leftbys']
                #if self.__conndict.get(ro) is None:
                #    break;
                left_bytes = self.__conndict.get(ro).get('leftbys')
                if left_bytes is None:
                    break

                if len(left_bytes):
                    self._log("here got a left bytes who's len is not zero, when sock recv.",self.LOG_DEBUG)

                #此处可能要优化的函数全局分支，避免每一个循环就要bytes()，除非python系统自己回收，并优化好
                left_bytes = bytes().join([left_bytes, recv_bytes])
                left_len = len(left_bytes)
                while left_len:#只要有数据，就循环处理
                    #一层解msg
                    try:
                        t,v_bytes,left_bytes = self.msgunpack(left_bytes)
                        #同时更新left_len
                        left_len = len(left_bytes)
                    except self.msgError as e:
                        if e.value == self.MSGERR_PACKLEN_LESS_TL:
                            self._log("unpack a pack who's len(%d) is not size up to the t+l"%(len(left_bytes)),
                                      self.LOG_DEBUG)
                        elif e.value == self.MSGERR_LENINPACK_NOT_SATISFIED:
                            self._log("unpack a pack who's len is not satisfied. ", self.LOG_DEBUG)
                        else:
                            pass
                        break

                    #二层解
                    if t == self.MSG_C_AUTH_REQ:
                        id,passsword = self.msgunpack_c_auth_req(v_bytes)
                        self._log('auth request:id =%d'%id)
                        self.__auth(ro,id,passsword)
                    elif t == self.MSG_C_UPLIST_REQ:
                        id, = self.msgunpack_c_uplist_req(v_bytes)
                        self.__proc_uplistreq(ro,id)

                    elif t == self.MSG_D_DIALOG_EXCH:
                        sayswho, saysto,says_bytes = self.msgunpack_d_dialog_exch(v_bytes)
                        #这里执行查找转发
                        self._log('says exchange: %d says to %d' % (sayswho,saysto), self.LOG_INFO)

                        if saysto == 0:#广播处理
                            send_bytes = self.msgpack_d_dialog_group(sayswho, 0, says_bytes)
                            self._log('send broadcast msg,all keys to:%s' % str(self.__conndict.keys()), self.LOG_DEBUG)
                            dictkeys = self.__idconndict.keys()
                            for i in dictkeys:
                                if i is not sayswho:#不给自己发
                                    #这里是要判断的wsolist是否满足，？？？！！！，这里
                                    saystoso = self.__idconndict.get(i)
                                    if saystoso is not None:
                                        self.__sosendall(saystoso,send_bytes)
                        else:  #单播交换
                            send_bytes = self.msgpack_d_dialog(sayswho, says_bytes)
                            saystoso = self.__idconndict.get(saysto)
                            if saystoso is not None:
                                self.__sosendall(saystoso, send_bytes)
                            else:
                                self._log('says exchange exception: saysto so which in idconndict is None.')
                    else:
                        #收到不认识的包，丢弃
                        self._log('unknow msg ,dropped.')

                #本次接收的数据消息处理结束
                #更新该连接用户的left_bytes,供其后续处理
                if self.__conndict.get(ro):
                    self.__conndict[ro]['leftbys'] = left_bytes

    def __proc_xlist(self,xlist):
        for xo in xlist:  # 处理异常socket
            self._log("here got a exception client socket,which is %s" % (str(xo)))
            xo.close()  # 主动将该异常so关闭
            self.__esolist.remove(xo)
            self._log('the exception client socket has been closed.')

if __name__  == "__main__":
    '''
    class test(serbase):
        def __init__(self):
            print('super()',super())
            super().__init__()
            super(test,self).__init__()
            print('test init')

    o = test()
    '''


